using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Text;

namespace cssp
{
	/// <summary>
	/// Resolves symbol values, and symbol references, and updates the unit text.
	/// After this the CSS is ready to be written to disk.
	/// </summary>
	public class CssxProcessor
	{
		static Regex mSymbolReferenceRegex = new Regex(@"\$[\w]+", RegexOptions.IgnoreCase | RegexOptions.Compiled);
		CssxParser mParser;
		List<Unit> mUnits;
		Unit mUnit;	// current unit

		public CssxProcessor()
		{
			mParser = new CssxParser();
			mParser.Scanner = new CssxScanner();
			mUnits = new List<Unit>();
		}

		/// <summary>
		/// Replaces symbol references throughout the unit text.
		/// Units with includes should be processed in bottom-up order to ensure incuded symbols
		/// are defined and resolved by the time they are referenced.
		/// </summary>
		/// <param name="unit"></param>
		/// <returns>processed unit</returns>
		public Unit Process(string filename)
		{
			#region Sanity check

			if (String.IsNullOrEmpty(filename))
				throw new ArgumentException("filename cannot be null or empty.");

			#endregion

			try
			{
				// recursively parse, and process unit and its imports (if any)
				ProcessUnit(filename);

				// if parsing failed don't try to resolve anything, just return the unit
				if (mUnits.Count == 0)
					return mUnit;

				mUnit = mUnits[0];

				// Resolve all symbol values
				if (Log.Logger.LogLevel >= LogLevel.Debug)
					Log.WriteLine(LogLevel.Debug, "----- Symbol Definitions: -----");
				foreach (Unit unit in mUnits)
				{
					foreach (Symbol symbol in unit.Symbols)
					{
						ResolveSymbolValue(symbol);
					}

					if (Log.Logger.LogLevel >= LogLevel.Debug)
					{
						string unitFilename = Path.GetFileName(unit.Filename);
						foreach (Symbol s in unit.Symbols)
							Log.WriteLine(LogLevel.Debug, String.Concat(unitFilename, "(", s.StartLineNum, "): ", s.Name, "='", s.Value, "'"));
					}
				}

				string text = mUnit.Text;
				text = mSymbolReferenceRegex.Replace(text, GetSymbolValue);
				mUnit.Text = text;

				return mUnit;
			}
			finally
			{
				mUnits.Clear();
			}
		}

		bool ProcessUnit(string filename)
		{
			string fileName = Path.GetFileName(filename);

			// Don't process the same file a second time
			if (GetProcessedUnit(filename) != null)
				return true;

			using (StreamReader reader = new StreamReader(filename))
			{
				mUnit = mParser.Parse(filename, reader);
			}

			if (mUnit.HasErrors)
			{
				StringBuilder sb = new StringBuilder();
				sb.Append(filename).Append(" parsed with errors, and will not be processed further.");
				if (mUnit.Filename.EndsWith(".cssi"))
					sb.Append(" Any units importing it will allso not be processed.");
				Log.WriteLine(LogLevel.Error, sb.ToString());
				return false;
			}

			// Make sure unit's symbols are not already defined in other units

			string importName = String.Empty;
			foreach (Symbol symbol in mUnit.Symbols)
			{
				if (GetExternalSymbol(symbol.Name, ref importName) != null)
					OnError(symbol, "Symbol '" + symbol.Name + "' is already defined in '" + importName + "'.");
			}

			if (mUnit.HasErrors)
				return false;

			mUnits.Add(mUnit);

			//
			// Process unit's imports
			//

			string importRelativePath = Path.GetDirectoryName(filename);
			
			// Cache the current unit in a local variable because mUnit will be
			// overwritten as the imports are processed.
			Unit currentUnit = mUnit;
			
			foreach(CodeBlock import in currentUnit.Imports)
				if (!ProcessUnit(Path.Combine(importRelativePath, import.Name)))
				{
					// if one of the imports has errors, the unit processing should stop
					currentUnit.HasErrors = true;
					return false;
				}

			return true;
		}

		Unit GetProcessedUnit(string filename)
		{
			return mUnits.Find(delegate(Unit u) { return u.Filename == filename; });
		}

		/// <summary>
		/// Find and return a named symbol from the global symbol list.
		/// </summary>
		/// <param name="symbolName">symbol name</param>
		/// <param name="unitName">if not empty, restricts search to named unit; otherwise all units are searched. If the symbol is found the argument is set to the name of the unit where the symbol was found.</param>
		/// <returns>symbol or null</returns>
		Symbol GetExternalSymbol(string symbolName, ref string unitName)
		{
			Symbol symbol = null;
			if (unitName.Length == 0)
			{
				foreach (Unit unit in mUnits)
				{
					symbol = unit.Symbols.Find(delegate(Symbol s)
					{
						return String.Compare(s.Name, symbolName, true) == 0;
					});
					if (symbol != null)
					{
						unitName = unit.Filename;
						return symbol;
					}
				}
			}
			else
			{
				Unit unit = GetProcessedUnit(unitName);
				if (unit != null)
				{
					symbol = unit.Symbols.Find(delegate(Symbol s)
					{
						return String.Compare(s.Name, symbolName, true) == 0;
					});
					if (symbol != null)
						return symbol;
				}
			}

			return symbol;
		}

		/// <summary>
		/// Find and return a named symbol from the global symbol list.
		/// </summary>
		/// <param name="symbolName">symbol name</param>
		/// <returns>symbol or null</returns>
		Symbol GetExternalSymbol(string symbolName)
		{
			string unitName = String.Empty;
			return GetExternalSymbol(symbolName, ref unitName);
		}

		string GetSymbolValue(Match m)
		{
			string name = m.Value.Substring(1);
			Symbol symbol = GetExternalSymbol(name);
			if (symbol != null)
				return symbol.Value;
			else
			{
				// it's not an external symbol, look in the current unit's symbols
				symbol = mUnit.Symbols.Find(delegate(Symbol s)
				{
					return String.Compare(s.Name, name, true) == 0;
				});
				if (symbol == null)
					OnError("Reference to undefined symbol: " + name);
				return m.Value;
			}
		}

		string ResolveSymbolValue(Symbol symbol)
		{
			if (!symbol.Value.Contains("$"))
				return symbol.Value;

			MatchCollection matches = Regex.Matches(symbol.Value, @"\$([\w]+)");
			List<string> values = new List<string>();
			foreach (Match m in matches)
			{
				Symbol s = GetExternalSymbol(m.Groups[1].Value);
				string value = ResolveSymbolValue(s);
				values.Add(value);
			}

			for (int i = 0; i < matches.Count; i++)
			{
				symbol.Value = symbol.Value.Replace(matches[i].Value, values[i]);
			}
			return symbol.Value;
		}

		void OnError(string message)
		{
			mUnit.HasErrors = true;
			Log.WriteLine(LogLevel.Error, Path.GetFileName(mUnit.Filename) + ": " + message);
		}

		void OnError(CodeBlock codeBlock, string message)
		{
			mUnit.HasErrors = true;
			Log.WriteLine(LogLevel.Error, Path.GetFileName(mUnit.Filename) + "(" + codeBlock.StartLineNum + "): " + message);
		}
	}
}
